/* 
 * Copyright (C) 2004-2005 Jonathan Bindel
 * Copyright (C) 2006-2007 Eskil Bylund
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

using System;
using System.Diagnostics;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;

using DCSharp.Backend.Connections;
using DCSharp.Backend.Objects;

namespace DCSharp.Backend.Protocols.Nmdc
{
	public class HubProtocol : UdpProtocol, IHubProtocol
	{
		[Flags]
		public enum Features
		{
			UserCommand = 0x01,
			NoGetINFO = 0x02,
			NoHello = 0x04,
			UserIP2 = 0x08,
			GetZBlock = 0x10,
			TTHSearch = 0x20,
			ZPipe0 = 0x40
		}

		private HubConnection connection;
		private IHubProtocolHelper helper;

		private Features hubFeatures;

		public HubProtocol(HubConnection connection, IHubProtocolHelper helper) :
			base(helper, helper)
		{
			if (connection == null)
			{
				throw new ArgumentNullException("connection");
			}
			if (helper == null)
			{
				throw new ArgumentNullException("helper");
			}
			this.connection = connection;
			this.helper = helper;

			// General
			commands.Add("$Supports", HandleSupports);
			commands.Add("$To:", HandleTo);
			commands.Add("$MyINFO", HandleMyInfo);
			commands.Add("$GetNetInfo", HandleGetNetInfo);
			commands.Add("$Quit", HandleQuit);

			// Connect
			commands.Add("$RevConnectToMe", HandleRevConnectToMe);
			commands.Add("$ConnectToMe", HandleConnectToMe);
			commands.Add("$ForceMove", HandleForceMove);

			// User
			commands.Add("$NickList", HandleNickList);
			commands.Add("$OpList", HandleOpList);

			// Search
			commands.Add("$Search", HandleSearch);
		}

		#region Properties

		public HubConnection Connection
		{
			get { return connection; }
			set { connection = value; }
		}

		public IHubProtocolHelper Helper
		{
			get { return helper; }
		}

		#endregion

		#region Methods

		public override void Handle(string message)
		{
			if (message.StartsWith("$"))
			{
				base.Handle(message);
			}
			else
			{
				HandleMessage(message);
			}
		}

		#region IHubProtocol Members

		public void RequestConnection(User user)
		{
			Identity identity = connection.Users[user];
			if (identity == null)
			{
				throw new InvalidOperationException("User offline.");
			}
			if (helper.ConnectionSettings.SupportsIncoming)
			{
				ConnectToMe(identity.Nick);
			}
			else
			{
				RevConnectToMe(identity.Nick);
			}
		}

		public void Search(SearchInfo searchInfo)
		{
			if (searchInfo == null)
			{
				throw new ArgumentNullException("searchInfo");
			}

			string from;
			if (helper.ConnectionSettings.SupportsIncoming)
			{
				from = String.Format("{0}:{1}", helper.ConnectionSettings.Address,
					helper.ConnectionSettings.Port);
			}
			else
			{
				from = String.Format("Hub:{0}", EscapeNick(connection.LocalIdentity.Nick));
			}
			Send("$Search {0} {1}|", from, GetSearchString(searchInfo));
		}

		public void SendMessage(string message)
		{
			if (message == null)
			{
				throw new ArgumentNullException("message");
			}
			Send("<{0}> {1}|", EscapeNick(connection.LocalIdentity.Nick),
				Escape(message));
		}

		public void SendPrivateMessage(User user, string message)
		{
			Identity identity = connection.Users[user];
			if (identity == null)
			{
				throw new InvalidOperationException("User offline.");
			}
			if (message == null)
			{
				throw new ArgumentNullException("message");
			}
			Send("$To: {0} From: {1} $<{1}> {2}|", EscapeNick(identity.Nick),
				EscapeNick(connection.LocalIdentity.Nick), Escape(message));

			// Emitting event since private messages aren't echoed back
			connection.OnPrivateMessage(connection.LocalIdentity, identity, message);
		}

		#endregion

		protected override void Invoke(string command, string argument)
		{
			switch (command)
			{
				// Handshake
				case "$Lock":
					if (argument.StartsWith("EXTENDEDPROTOCOL"))
					{
						Send("$Supports NoGetINFO NoHello TTHSearch|");
					}
					SendKey(connection, CalculateKey(argument, Encoding));
					Send("$ValidateNick {0}|", EscapeNick(connection.LocalIdentity.Nick));
					break;

				case "$HubName":
					int separatorPos = argument.IndexOf(" - ");
					if (separatorPos > 1)
					{
						connection.Name = argument.Substring(0, separatorPos);
						connection.Topic = argument.Substring(separatorPos + 3);
					}
					else
					{
						connection.Name = argument;
						connection.Topic = null;
					}
					break;

				case "$ValidateDenide": // [sic]
					// TODO: Display an error message to the user.
					connection.Disconnect();
					break;

				case "$GetPass":
					Send("$MyPass {0}|", connection.Password);
					break;

				case "$BadPass":
					connection.Disconnect();
					break;

				case "$LogedIn": // [sic]
					// This command is sent to ops after a successful $MyPass.
					if (UnescapeNick(argument) == connection.LocalIdentity.Nick)
					{
						connection.LocalIdentity.Op = true;
					}
					break;

				case "$Hello":
					if (UnescapeNick(argument) == connection.LocalIdentity.Nick)
					{
						Send("$Version 1,0091|");
						Send("$GetNickList|");
						Send("$MyINFO $ALL {0}|", GetIdentityInfo());
						connection.SetStateConnected();
					}
					break;

				default:
					base.Invoke(command, argument);
					break;
			}
		}

		protected override bool GetUser(string nick, string hubName,
			string hubAddress, out User user, out HubConnection hub)
		{
			Identity identity = GetIdentity(nick);

			hub = Connection;
			user = identity != null ? identity.User : null;
			return user != null;
		}

		protected void Send(string format, params object[] args)
		{
			connection.Send(args != null ? String.Format(format, args) : format);
		}

		protected bool Supports(Features feature)
		{
			return hubFeatures == (hubFeatures | feature);
		}

		protected Identity GetIdentity(string nick)
		{
			return GetIdentity(connection, nick);
		}

		protected Identity CreateIdentity(string nick)
		{
			if (connection.LocalIdentity.Nick == nick)
			{
				return connection.LocalIdentity;
			}
			Uid cid = MakeCid(nick, connection.Address);
			return new Identity(User.Create(cid, nick), nick);
		}

		protected string GetIdentityInfo()
		{
			Identity identity = connection.LocalIdentity;

			string mode = helper.ConnectionSettings.SupportsIncoming ? "A" : "P";
			int hubsNotRegistered = 1;
			int hubsRegistered = 0;
			int hubsOp = 0;
			//int maxToOpen = 40;

			StringBuilder tag = new StringBuilder();
			tag.AppendFormat("<++ V:0.674,M:{0},H:{1}/{2}/{3},S:{4}>", mode,
				hubsNotRegistered, hubsRegistered, hubsOp, helper.Slots);
			//tag.Append(",O:");
			//tag.Append(maxToOpen);

			StringBuilder info = new StringBuilder();
			info.AppendFormat("{0} {1}{2}$ ${3}{4}${5}${6}$",
				EscapeNick(identity.Nick), Escape(identity.Description), tag,
				Escape(identity.Connection), (char)1, Escape(identity.Email),
				helper.ShareTotal);

			return info.ToString();
		}

		protected static string GetSearchString(SearchInfo searchInfo)
		{
			// <sizerestricted>?<ismaximumsize>?<size>?<datatype>?<searchpattern>

			// <sizerestricted> is 'T' if the search should be restricted to files
			//	of a minimum or maximum size, otherwise 'F'.
			// <ismaximumsize> is 'F' if <sizerestricted> is 'F' or if the size
			//	restriction places a lower limit on file size, otherwise 'T'.
			// <size> is the minimum or maximum size of the file to report
			//	(according to <ismaximumsize>) if <sizerestricted> is 'T', otherwise 0.
			// <searchpattern> is used other users to determine if any files match.
			//	Non-alphanumeric characters (including spaces and periods) are replaced by '$'.
			//	The server must forward this message unmodified to all the other users. Every other
			//	user with one or more matching files must send a UDP packet to <ip>:<port>

			long size = searchInfo.MaxSize ?? (searchInfo.MinSize ?? 0);
			bool isMaximumSize = searchInfo.MaxSize != null;

			StringBuilder message = new StringBuilder();

			message.AppendFormat("{0}?{1}?{2}?", size > 0 ? "T" : "F",
				isMaximumSize ? "T" : "F", size);

			if (searchInfo.TTH == null)
			{
				string keywords = String.Join(" ", searchInfo.Keywords);
				message.AppendFormat("{0}?{1}", (int)searchInfo.Type,
					Regex.Replace(keywords, @"\W", "$"));
			}
			else
			{
				// DC++ uses 9 as the datatype for TTH
				message.AppendFormat("{0}?TTH:{1}", 9, searchInfo.TTH);
			}
			return message.Append("|").ToString();
		}

		protected string CreateResultString(SearchResult result, string nick)
		{
			// $SR <source_nick> <result> <free_slots>/<total_slots><0x05><hub_name> (<hub_ip:listening_port>)[<0x05><target_nick>]|
			// <result> is one of the following: 
			// 	* <file_name><0x05><file_size> for file results
			//  * <directory> for directory results
			// For files containing TTH, the <hub_name> parameter is replaced with TTH:<base32_encoded_tth_hash>

			string nameOrTTH = result.TTH != null ? "TTH:" + result.TTH : connection.Name;

			StringBuilder message = new StringBuilder();
			message.AppendFormat("$SR {0} {1}", connection.LocalIdentity.Nick,
				result.Path);

			if (result.Type == ResultType.File)
			{
				message.Append((char)5);
				message.Append(result.Size);
			}

			message.AppendFormat(" {0}/{1}{2}{3} ({4})", helper.FreeSlots, helper.Slots,
				(char)5, nameOrTTH, connection.Address);

			if (nick != null)
			{
				message.AppendFormat("{0}{1}", (char)5, nick);
			}
			return message.Append("|").ToString();
		}

		#region Outgoing
		
		protected void ConnectToMe(string nick)
		{
			Send("$ConnectToMe {0} {1}:{2}|", EscapeNick(nick),
				helper.ConnectionSettings.Address, helper.ConnectionSettings.Port);
		}

		protected void RevConnectToMe(string nick)
		{
			Send("$RevConnectToMe {0} {1}|", EscapeNick(connection.LocalIdentity.Nick),
				EscapeNick(nick));
		}

		#endregion

		#region Incoming

		private void HandleMessage(string message)
		{
			Identity user = null;

			int beg = message.IndexOf('<');
			int end = message.IndexOf('>');
			if (beg == 0 && end > 0)
			{
				string nick = UnescapeNick(message.Substring(1, end - 1));

				user = GetIdentity(nick) ?? CreateIdentity(nick);
				message = message.Substring(end + 2);
			}
			connection.OnMessage(user, Unescape(message));
		}

		#region General

		private void HandleSupports(string argument)
		{
			string[] supports = argument.Split(' ');
			foreach (string feature in supports)
			{
				if (feature.Length <= 0)
				{
					continue;
				}
				try
				{
					Features feat = (Features)Enum.Parse(typeof(Features),
						feature);

					hubFeatures |= feat;
				}
				catch (ArgumentException)
				{
					Debug.WriteLine("Unknown feature " + feature);
				}
			}
		}

		private void HandleTo(string argument)
		{
			//<othernick> From: <nick> $<<nick>> <message>
			int beg = argument.IndexOf("From: ") + 6;
			int end = argument.IndexOf('$');
			if (beg < 6 || end <= beg || argument.Length <= end + 3)
			{
				return;
			}

			string nick = UnescapeNick(argument.Substring(beg, end - beg).Trim());
			string message = argument.Substring(end + 1);

			// TODO: Proper handling of "group chats". For now the secondary nick is kept in the message.
			end = message.IndexOf('>');
			if (message[0] == '<' && end > 1)
			{
				string n = message.Substring(1, end - 1);
				if (n == nick)
				{
					message = message.Substring(end + 1);
				}
			}

			Identity identity = GetIdentity(nick) ?? CreateIdentity(nick);

			connection.OnPrivateMessage(identity, connection.LocalIdentity,
				Unescape(message.Trim()));
		}

		private void HandleMyInfo(string argument)
		{
			if (argument.StartsWith("$ALL "))
			{
				argument = argument.Substring(5);
			}
			string nick = UnescapeNick(Regex.Match(argument, @"([^\s^<])*").Value);

			Identity user = GetIdentity(nick);
			if (user == null)
			{
				user = CreateIdentity(nick);
				ParseMyInfo(user, argument);

				connection.Users.Add(user);
			}
			else
			{
				ParseMyInfo(user, argument);
				connection.Users.EmitUserChanged(user);
			}
		}

		private void HandleGetNetInfo(string argument)
		{
			char mode = helper.ConnectionSettings.SupportsIncoming ? 'A' : 'P';

			Send("$NetInfo {0}${1}{2}|", helper.Slots, 1, mode);
		}

		private void HandleQuit(string argument)
		{
			Identity user = GetIdentity(UnescapeNick(argument));
			if (user != null)
			{
				//user.Online = false;
				connection.Users.Remove(user);
			}
		}

		#endregion

		#region Connect

		private void HandleRevConnectToMe(string argument)
		{
			if (helper.ConnectionSettings.SupportsIncoming)
			{
				string[] strings = argument.Split(' ');
				if (strings.Length > 0)
				{
					string nick = strings[0];
					ConnectToMe(nick);
				}
			}
		}

		private void HandleConnectToMe(string argument)
		{
			string[] strings = argument.Split(' ');
			if (strings.Length != 2)
			{
				return;
			}

			string[] addressAndPort = strings[1].Split(':');

			IPAddress address;
			int port;

			if (addressAndPort.Length == 2 &&
				IPAddress.TryParse(addressAndPort[0], out address) &&
				int.TryParse(addressAndPort[1], out port))
			{
				IPEndPoint endPoint = new IPEndPoint(address, port);

				UserConnection connection = helper.CreateUserConnection(endPoint,
					ConnectionProtocol.Nmdc);

				connection.Hub = this.connection;
				connection.LocalIdentity = this.connection.LocalIdentity;
				connection.Encoding = this.connection.Encoding;

				connection.Connect();
			}
		}

		private void HandleForceMove(string argument)
		{
			Uri target;
			if (StringUtil.TryCreateUri(argument, out target))
			{
				connection.OnRedirected(target);
			}
			connection.Disconnect();
		}

		#endregion

		#region User

		private void HandleNickList(string argument)
		{
			HandleNickList(argument, false);
		}

		private void HandleOpList(string argument)
		{
			HandleNickList(argument, true);
		}

		private void HandleNickList(string argument, bool isOpList)
		{
			string[] nicks = Regex.Split(argument, @"\${2}");
			foreach (string n in nicks)
			{
				if (n.Length <= 0)
				{
					continue;
				}
				bool getInfo = false;
				string nick = UnescapeNick(n);

				Identity identity = GetIdentity(nick);
				if (identity == null)
				{
					identity = CreateIdentity(nick);
					identity.Op = isOpList;

					connection.Users.Add(identity);
					getInfo = !Supports(Features.NoGetINFO);
				}
				else if (isOpList && !identity.Op)
				{
					identity.Op = true;
					connection.Users.EmitUserChanged(identity);
				}
				if (getInfo)
				{
					Send("$GetINFO {0} {1}|", EscapeNick(nick),
						EscapeNick(connection.LocalIdentity.Nick));
				}
			}
		}

		#endregion

		#region Search

		private void HandleSearch(string argument)
		{
			// $Search <ip>:<port> <searchstring>
			// $Search Hub:<requestornick> <searchstring>

			string[] parts = argument.Split(' ');
			if (parts.Length != 2)
			{
				return;
			}

			string[] from = parts[0].Split(':');
			if (from.Length != 2)
			{
				return;
			}

			IPEndPoint endPoint = null;
			string nick = null;

			if (from[0].Equals("Hub"))
			{
				nick = from[1];
				Identity identity = GetIdentity(nick);

				if (identity == null || identity == connection.LocalIdentity ||
					(helper.ConnectionSettings.SupportsIncoming == false &&
					identity.Active == false))
				{
					return;
				}
			}
			else
			{
				if (from[0] == helper.ConnectionSettings.Address &&
					from[1] == helper.ConnectionSettings.Port.ToString())
				{
					return;
				}
				endPoint = new IPEndPoint(IPAddress.Parse(from[0]),
					int.Parse(from[1]));
			}

			if (!helper.CanSearch(parts[0]))
				return;

			// <sizerestricted>?<ismaxsize>?<size>?<datatype>?<searchpattern>

			string[] searchString = parts[1].Split('?');
			if (searchString.Length < 5)
			{
				return;
			}

			long? maxSize = null;
			long? minSize = null;

			bool sizeRestrict = searchString[0].Equals("T");
			if (sizeRestrict)
			{
				bool isMaxSize = searchString[1].Equals("T");
				long? size = long.Parse(searchString[2]);

				maxSize = isMaxSize ? size : null;
				minSize = !isMaxSize ? size : null;
			}

			SearchFileType type = (SearchFileType)Enum.Parse(typeof(SearchFileType),
				searchString[3]);

			SearchInfo searchInfo;
			if ((int)type == 9)
			{
				// It's a TTH search
				string tth = searchString[4].Substring(4);

				searchInfo = new SearchInfo(tth);
			}
			else
			{
				string pattern = searchString[4];
				string[] keywords = pattern.Split('$');

				searchInfo = new SearchInfo(keywords, type, maxSize,
					minSize);
			}

			// Search
			SearchResult[] results = helper.Search(searchInfo,
				endPoint != null ? 10 : 5);

			// Respond
			foreach (SearchResult result in results)
			{
				string message = CreateResultString(result, nick);
				if (endPoint != null)
				{
					UdpServer.Send(endPoint, message, Encoding);
				}
				else
				{
					Send(message);
				}
			}
		}

		#endregion

		private static void ParseMyInfo(Identity user, string info)
		{
			// <nick> <description>$<active>$<connection><flag>$<e-mail>$<sharesize>$|
			int i = info.IndexOf(' ');
			if (i < 0)
				return;

			int j = info.IndexOf('$', ++i);
			if (j < 0)
				return;

			string description = info.Substring(i, j - i);

			i = description.LastIndexOf('<');
			if (i >= 0 && description.Length > 0 && description[description.Length - 1] == '>')
			{
				string tag = description.Substring(i);
				description = description.Substring(0, i);

				user.SetParameter("tag", tag);
				UpdateFromTag(user, tag);
			}
			user.Description = NullOrUnescaped(description);

			i = j + 1;
			j = info.IndexOf('$', i);
			if (j < 0)
				return;

			// If set to active by the tag, don't set it here
			if (!user.Active)
			{
				user.Active = info.Substring(i, j - i) == "A";
			}

			i = j + 1;
			j = info.IndexOf('$', i);
			if (j < 0)
				return;

			user.Connection = NullOrUnescaped(info.Substring(i, j - i - 1));

			i = j + 1;
			j = info.IndexOf('$', i);
			if (j < 0)
				return;

			user.Email = NullOrUnescaped(info.Substring(i, j - i));

			i = j + 1;
			j = info.IndexOf('$', i);
			if (j < 0)
				return;

			long shareSize;
			if (long.TryParse(info.Substring(i, j - i), out shareSize))
			{
				user.ShareSize = shareSize;
			}
		}

		private static void UpdateFromTag(Identity identity, string tag)
		{
			if (tag.Length < 3 || tag.IndexOf(' ') < 1)
			{
				return;
			}
			tag = tag.Substring(1, tag.Length - 2);

			string[] parts = tag.Split(' ');
			string client = parts[0];

			foreach (string pair in parts[1].Split(','))
			{
				if (pair.Length < 3)
				{
					continue;
				}
				char key = pair[0];
				string value = pair.Substring(2, pair.Length - 2);

				// TODO: Handle the keys 'H' and 'S'.
				switch (key)
				{
					case 'M':
						identity.Active = value == "A";
						break;

					case 'V':
						identity.Client = GetClient(client, value); 
						break;
				}
			}
		}

		private static string GetClient(string client, string version)
		{
			// TODO: Update with any missing clients.
			switch (client)
			{
				case "++":
					return "DC++ " + version;

				case "StrgDC++":
					return "StrongDC++ " + version;

				case "SP":
					return "ShakesPeer " + version;
			}
			return String.Format("{0} {1}", client, version);
		}

		private static string NullOrUnescaped(string text)
		{
			return String.IsNullOrEmpty(text) ? null : Unescape(text);
		}

		#endregion

		#endregion
	}
}
